module PannelloModule {

    //
    // dependencies: 
    // -jquery
    // -jqueryUI

    export class Pannello {
        private options: PannelloOptions;
        private panelState: PanelState;

        private panelWidth: number;
        private jPanel: JQuery;

        private jApplication: JQuery;
        private jPanelContent: JQuery;
        private jPanelContentCurrentOpacity: number;

        private jHandler: JQuery;
        private jHandlerBox: JQuery;

        private dragging: boolean;
        private queueAnimations: boolean = false;

        //
        // Hack inspired by http://stackoverflow.com/a/2526029/1904983
        // to prevent click callback on drag
        // 
        private _noClickOnHandlerBox: boolean;
        private _noClickOnHandlerWrapper: boolean;

        //Prevents multiple binding
        private _globalEventsBound: boolean;

        //
        // Width in percentage of the invisible handler
        // The handler will catch the dragging gestures
        //
        //deprecated?
        //private handlerDefaultWidth: number = 50;

        //
        // The 2 followings property define the minimum offset necessary
        // to either auto close the panel or auto open it
        //
        private offsetToAutoClosePx: number;
        private offsetToAutoOpenPx: number;

        private lastPositionLeft: number;
        private direction: PannelloModule.DragDirection;

        private log: (msg: string) => void;

        constructor(options: PannelloModule.PannelloOptions) {

            this.options = options;

            this.SetDefaults();
            this.AttachDragging();

        }

        private SetDefaults(): void {
            this.jPanel = $(this.options.panelElSelector);
            this.jPanelContent = this.jPanel.children(":first");

            this.jHandler = $(this.options.handlerSelector);
            this.jHandlerBox = this.jHandler.children(":first");

            //figure out behavior
            if (typeof this.options.behavior === "undefined") {
                this.options.behavior = PannelloModule.PanelBehavior.Overlay;
            }

            else if (this.options.behavior === PannelloModule.PanelBehavior.Push) {
                this.jApplication = $(this.options.appSelector);
                this.queueAnimations = false;
            }

            //setup logging
            if (this.options.debugMode) {

                if (this.options.debugFunction) {
                    this.log = this.options.debugFunction;

                }

                else {
                    this.log = (msg: string): void => {
                        console.log(msg);
                    }
                }
            }

            //do nothing, not debug mode
            else {
                this.log = (msg: string): void=> { ; }
            }

            if (!this.options.percentagePanelWidth || this.options.percentagePanelWidth <= 0 || this.options.percentagePanelWidth > 100) {
                this.options.percentagePanelWidth = 70;

            }

            this.SetPanelWidth();

            if (this.options.propagateClickBehindHandler) {
                //setup click manager on invisible handler
                this.jHandler.click((event: JQueryEventObject) => {
                    return this.HandleClickOnHandlerWrapper(event);
                });
            }

            //setup click event on handler box
            this.jHandlerBox.click((event: JQueryEventObject) => {
                this.HandleClickOnHandlerBox(event);
            });

            ////handler is half screen wide
            //this.jHandler.css("width", window.outerWidth * (this.handlerDefaultWidth / 100));

            //closed by default
            this.Close(true);


            //animation duration
            if (!this.options.animationsMs) {
                this.options.animationsMs = 0;
            }

        }

        private AttachDragging(): void {

            var dragOptions: DraggableCustomOptions = {
                axis: "x",
                handle: this.options.handlerSelector + ", ." + this.jPanelContent.attr("class"),
                distance: 5,
                containment: [0 - this.panelWidth, 0, 0, 0],
                start: (event: Event, ui: JQueryUI.DraggableEventUIParams): void=> {
                    return this.StartedDragging(event, ui);
                },
                drag: (event: Event, ui: JQueryUI.DraggableEventUIParams) => {
                    this.Dragging(event, ui);
                },
                stop: (event: Event, ui: JQueryUI.DraggableEventUIParams): void => {
                    this.StoppedDragging(event, ui);
                }
            };

            //Set draggability
            this.jPanel.draggable(dragOptions);
            this.Close(true);

        }

        public RefreshPanelWidth(): void {
            this.log("Resize panel");
            this.SetPanelWidth();

        }

        private SetPanelWidth(): void {

            var width = window.outerWidth * (this.options.percentagePanelWidth / 100);

            this.jPanel.css("width", width + "px");
            this.jHandler.css("left", width + "px");

            this.panelWidth = width;

            if (this.panelState === PannelloModule.PanelState.Close) {
                this.jPanel.css("left", (0 - this.panelWidth) + "px");
            }
            else {
                this.jPanel.css("left", 0 + "px");
            }


            //TODO: check that the values are valid
            //TODO: est if these work as expected
            this.offsetToAutoClosePx = this.panelWidth * (100 - this.options.percentageDragToAutoClose) / 100;
            this.offsetToAutoOpenPx = this.panelWidth * this.options.percentageDragToAutoOpen / 100;

            if (this.options.behavior === PannelloModule.PanelBehavior.Push && this.panelState === PannelloModule.PanelState.Open) {
                this.jApplication.css("marginLeft", this.panelWidth + "px");
            }
        }

        public GetPanelState(): PanelState {
            return this.panelState;
        }

        public Refresh(newOptions?: PannelloOptions): void {
            //Reset options if provided. Then refresh draggaility

            if (newOptions) {
                this.options = newOptions;
            }

            this.AttachDragging();
        }

        private HandleClickOnHandlerWrapper(event: JQueryEventObject): boolean {

            if (!this.dragging) {

                this.log("Click/tap behind the handler happened. Do magic");

                this.jHandler.hide();
                var el = document.elementFromPoint(event.pageX, event.pageY);
                $(el).trigger(event.type);
                this.jHandler.show();
                event.stopImmediatePropagation();
                return false;
            }
            else {
                return true;
            }

        }

        private HandleClickOnHandlerBox(event: JQueryEventObject): void {

            if (!this._noClickOnHandlerBox) {
                this.log("Click/tap on handler");

                this.Toggle();
            }

            else {
                this._noClickOnHandlerBox = false;

            }
        }

        private BindClickOnDocument(): void {

            if (!this._globalEventsBound) {

                //Close panel if open when clicking on anything but the panel
                $('html').bind("click.document", (event: JQueryEventObject): void=> {
                    this.ClickOnDocumentHandler();
                });

                $('html').bind("keyup.pannello", (event: JQueryKeyEventObject): void=> {

                    //esc pressed
                    if (event.which === 27) {
                        this.log("Esc pressed");
                        this.ClickOnDocumentHandler();
                    }
                });

                //http://stackoverflow.com/questions/152975/how-to-detect-a-click-outside-an-element?page=1&tab=votes#tab-top
                this.jPanelContent.bind("click.panel", (event) => {
                    event.stopPropagation();
                });

                this._globalEventsBound = true;
            }
        }

        private UnbindClickOnDocument(): void {

            if (this._globalEventsBound) {
                //Close panel if open when clicking on anything but the panel
                $('html').unbind("click.document");

                $('html').unbind("keyup.pannello");

                //http://stackoverflow.com/questions/152975/how-to-detect-a-click-outside-an-element?page=1&tab=votes#tab-top
                this.jPanelContent.unbind("click.panel");

                this._globalEventsBound = false;
            }
        }

        private ClickOnDocumentHandler(): void {

            if (!this._noClickOnHandlerWrapper) {
                this.log("Click/tap outside pannello");

                this.Close();
                event.stopPropagation();
            }
            else {
                this._noClickOnHandlerWrapper = false;
            }
        }

        private StartedDragging(event: Event, ui: JQueryUI.DraggableEventUIParams): void {

            this.dragging = true;
            this._noClickOnHandlerBox = true;
            this.lastPositionLeft = this.panelWidth - Math.abs(ui.position.left);

            //
            //Hack: If the panel is open that means there is a click handler on the document.
            //On stopped dragging that event's callback will be called if we don't prevent it
            //
            if (this.panelState === PannelloModule.PanelState.Open) {
                this._noClickOnHandlerWrapper = true;
            }


            this.log("Dragging on pannello has now started");

            if (this.options.start) {
                this.options.start();
            }


        }

        private StoppedDragging(event: Event, ui: JQueryUI.DraggableEventUIParams): boolean {

            this.lastPositionLeft = this.panelWidth - Math.abs(ui.position.left);

            if (this.panelState === PannelloModule.PanelState.Close && this.MustAutoOpen()) {
                this.Open();
            }

            else if (this.panelState === PannelloModule.PanelState.Open && this.MustAutoClose()) {
                this.Close();
            }
            else {
                this.RevertDragAction();

                event.stopImmediatePropagation();
                return false;
            }

            return true;
        }

        private Dragging(event: Event, ui: JQueryUI.DraggableEventUIParams): void {


            var newPositionLeft = this.panelWidth - Math.abs(ui.position.left);
            var offsetDrag = Math.abs(this.lastPositionLeft - newPositionLeft);
            var marginValue: string;


            var opacityOffset = this.CalculateOpacityOffset(offsetDrag);

            //dragging right, opening
            if (newPositionLeft > this.lastPositionLeft) {
                this.direction = PannelloModule.DragDirection.Right;
                marginValue = "+=" + offsetDrag + "px";

                if (this.options.opacityAnimation) {
                    this.jPanelContentCurrentOpacity = this.jPanelContentCurrentOpacity + opacityOffset;
                    this.jPanelContent.css("opacity", this.jPanelContentCurrentOpacity);
                }
            }

            //dragging left, closing
            else {
                this.direction = PannelloModule.DragDirection.Left;
                marginValue = "-=" + offsetDrag + "px";

                if (this.options.opacityAnimation) {
                    this.jPanelContentCurrentOpacity = this.jPanelContentCurrentOpacity - opacityOffset;
                    this.jPanelContent.css("opacity", this.jPanelContentCurrentOpacity);
                }
            }

            if (this.options.behavior === PannelloModule.PanelBehavior.Push) {

                //Pull back the application
                this.jApplication.css("marginLeft", marginValue);
            }


            this.lastPositionLeft = newPositionLeft;

            if (this.options.drag) {
                this.options.drag(this.direction, offsetDrag, this.lastPositionLeft);
            }
        }

        private MustAutoClose(): boolean {

            this.log("Calculating threshold on closing. Current value: " + this.lastPositionLeft + ". Auto-close happens when current value is less than " + this.offsetToAutoClosePx);

            if (this.lastPositionLeft <= this.offsetToAutoClosePx) {
                return true;
            }
            else {
                return false;
            }
        }

        public Close(immediate?: boolean) {

            if (this.options.animationsMs && !immediate) {

                var animateOptions: JQueryAnimationOptions = {
                    queue: this.queueAnimations,
                    duration: this.options.animationsMs,
                    complete: () => {
                        this.PanelClosedCallback();
                    }
                };

                //Close panel with animation
                this.jPanel.animate(
                    //props
                    {
                        left: (0 - this.panelWidth) + "px",
                        //,opacity: 0
                    },
                    animateOptions);

                if (this.options.behavior === PannelloModule.PanelBehavior.Push) {

                    //Pull back the application
                    this.jApplication.animate(
                        //props
                        {
                            marginLeft: 0,
                        },
                        {
                            queue: this.queueAnimations,
                            duration: this.options.animationsMs
                        });
                }
            }

            //Close panel immediately
            else {
                this.jPanel.css("left", (0 - this.panelWidth) + "px");

                if (this.options.behavior === PannelloModule.PanelBehavior.Push) {
                    this.jApplication.css("marginLeft", 0);
                }

                this.PanelClosedCallback();
            }
        }

        private PanelClosedCallback(): void {
            this.panelState = PannelloModule.PanelState.Close;

            this.dragging = false;

            this.jPanelContentCurrentOpacity = 0;
            this.jPanelContent.css("opacity", this.jPanelContentCurrentOpacity);

            this.UnbindClickOnDocument();

            this.log("Pannello now closed");

            if (this.options.stop) {
                this.options.stop(this.panelState);
            }
        }

        private MustAutoOpen(): boolean {

            this.log("Calculating threshold on opening. Current value: " + this.lastPositionLeft + ". Auto-open happens when current value is greather than " + this.offsetToAutoOpenPx);

            if (this.lastPositionLeft > this.offsetToAutoOpenPx) {
                return true;
            }
            else {
                return false;
            }
        }

        public Open(immediate?: boolean) {
            if (this.options.animationsMs && !immediate) {

                var animateOptions: JQueryAnimationOptions = {
                    queue: this.queueAnimations,
                    duration: this.options.animationsMs,
                    complete: () => {
                        this.PanelOpenedCallback();
                    }
                };

                //Open panel with animation
                this.jPanel.animate(
                    //props
                    {
                        left: "0px"
                        //,opacity: 1
                    },
                    animateOptions
                    );

                if (this.options.behavior === PannelloModule.PanelBehavior.Push) {

                    //Pull back the application
                    this.jApplication.animate(
                        //props
                        {
                            marginLeft: this.panelWidth + "px",
                        },
                        {
                            queue: this.queueAnimations,
                            duration: this.options.animationsMs
                        });
                }
            }

            else {
                //Open panel immediately
                this.jPanel.css("left", 0);

                if (this.options.behavior === PannelloModule.PanelBehavior.Push) {
                    this.jApplication.css("marginLeft", this.panelWidth + "px");
                }

                this.PanelOpenedCallback();
            }
        }

        public Toggle(immediate?: boolean) {

            if (this.panelState === PannelloModule.PanelState.Open) {
                this.Close(!!immediate);
            }

            else {
                this.Open(!!immediate);
            }

        }

        private PanelOpenedCallback(): void {
            this.panelState = PannelloModule.PanelState.Open;
            this.dragging = false;

            this.jPanelContentCurrentOpacity = 1;
            this.jPanelContent.css("opacity", this.jPanelContentCurrentOpacity);

            this.BindClickOnDocument();

            this.log("Pannello now opened");

            if (this.options.stop) {
                this.options.stop(this.panelState);
            }
        }

        private RevertDragAction() {

            if (this.panelState === PannelloModule.PanelState.Open) {
                this.Open();
            }

            else {
                this.Close();
            }
        }

        private CalculateOpacityOffset(offset: number): number {

            return offset / this.panelWidth;
        }

    }

    export enum PanelState {
        Open,
        Close
    }

    export enum DragDirection {
        Left,
        Right

    }

    export enum PanelBehavior {
        Overlay,
        Push
    }

    //hack to overcome a bug in the d.ts file
    export interface DraggableCustomOptions extends JQueryUI.DraggableOptions, JQueryUI.DraggableEvents {

    }
}